import datetime
import functools
import logging

from absl import app, flags
from tornado.ioloop import IOLoop, PeriodicCallback

from coin.exchange.base.order_gateway_config import convert_og_config_from_dict
from coin.flow.simple_queue import SimpleQueue
import coin.flow.subscriber as flow_subscriber
from coin.strategy.hk_hard_arb.util import load_clean_json
from coin.strategy.mm.base.bookmap import BookMap
from coin.strategy.mm.base.multi_feed_subsystem import MultiFeedSubsystem
from coin.strategy.mm.feed import (SubscriptionRequest, get_flow_book_builder_cls)
from coin.strategy.mm.live_envs import (create_live_env_instance, get_live_env_cls)
from coin.strategy.accounting.pnl_balance_printer.factory import get_pnl_balance_printer_cls
from coin.strategy.tool.fill_log_dumper import FillLogDumper

FLAGS = flags.FLAGS


class PnlPublisherConfig(object):
  def __init__(self, config_dict):
    _check_config_dict(config_dict)
    self.subreq = SubscriptionRequest.from_dict(config_dict['subreq'])
    product_type = get_flow_book_builder_cls(self.subreq).ProductType
    self.reserve = {
        product_type.FromStrProduct(symbol): (pos['min_pos'] + pos['max_pos']) / 2 for symbol,
        pos in config_dict['products'].items()
    }
    og_params = config_dict.get('og_params', {})
    self.og_params = convert_og_config_from_dict(self.subreq, og_params)


class LogPublishStrategy(object):
  def __init__(self, config):
    self._logger = logging.getLogger(__name__)
    assert isinstance(config, PnlPublisherConfig)
    self._config = config
    self._products = list(self._config.reserve.keys())
    assert len(self._products) > 0
    base_currencies = [product.base for product in self._products]
    if self._config.subreq.market_type == 'Futures':
      self._currencies = base_currencies
    else:
      quote_currency = self._products[0].quote
      self._currencies = base_currencies + [quote_currency]
    self._book = BookMap(self._products, logger=self._logger)
    reserve_map = {}
    if self._config.subreq.market_type == 'Spot':
      reserve_map = {
          product.base.currency: reserve for product, reserve in self._config.reserve.items()
      }
    printer_cls = get_pnl_balance_printer_cls(self._config.subreq)
    self._printer = printer_cls(self._products,
                                self._book,
                                reserve_map=reserve_map,
                                logger=self._logger)
    self._fill_dumper = FillLogDumper(self._config.subreq)

    self._ioloop = None
    self._queue = None
    self._flow_sub = None

    self._feed_subsystem = None
    self._og = None

  @property
  def products(self):
    return self._products

  @property
  def currencies(self):
    return self._currencies

  @property
  def subreq(self):
    return self._config.subreq

  def register_flow(self, ioloop, queue, flow_sub):
    self._ioloop = ioloop
    self._queue = queue
    self._flow_sub = flow_sub

  def register_feed_subsystem(self, feed_subsystem):
    self._feed_subsystem = feed_subsystem

  def register_order_gateway(self, name, order_gateway):
    self._logger.info('OG initialized: %s', name)
    if name == self._config.subreq:
      self._og = order_gateway
    else:
      raise ValueError('Unknown OG: %s' % name)
    self._ioloop.add_callback(self.on_og_reset)

  def on_og_reset(self):
    self._printer.register_order_gateway(self._og)
    self._og.register_fill_callback(self._fill_dumper.dump_fill)

  def prepare(self, feed_subsystem):
    assert feed_subsystem is not None
    for product in self._products:
      feed_subsystem.subscribe_book(product, functools.partial(self.on_book, product))
    PeriodicCallback(self._printer.print_pnl_balance, 30 * 1000).start()

  def on_book(self, product, book):
    self._book.set_book(product, book)


def _check_config_dict(config):
  assert 'subreq' in config
  assert 'products' in config
  symbol_set = set()
  for symbol in config['products'].keys():
    assert symbol not in symbol_set
    symbol_set.add(symbol)


def run_live(strategy, og_config_name, og_params, init_order_gateway_after=None, exit_after=None):
  # Initialize loop
  ioloop = IOLoop.current()
  queue = SimpleQueue('queue')
  flow_sub = flow_subscriber.from_queue(queue)
  strategy.register_flow(ioloop, queue, flow_sub)

  # Initialize feed
  feed_subsystem = MultiFeedSubsystem()
  strategy.register_feed_subsystem(feed_subsystem)

  # Initialize OG live environment
  env_cls = get_live_env_cls(strategy.subreq)
  env = create_live_env_instance(env_cls,
                                 currencies=strategy.currencies,
                                 trade_products=strategy.products,
                                 og_trade_key=og_config_name,
                                 og_params=og_params,
                                 init_og_after=init_order_gateway_after,
                                 ioloop=ioloop,
                                 queue=queue,
                                 flow_sub=flow_sub)
  env.on_order_gateway_reset_callback.append(strategy.register_order_gateway)

  # Start
  strategy.prepare(feed_subsystem)
  feed_subsystem.start(ioloop, queue, flow_sub)
  env.start()

  if exit_after is not None:
    ioloop.add_timeout(exit_after, ioloop.stop)

  try:
    ioloop.start()
    return 0
  except KeyboardInterrupt:
    return 1


def main(argv):
  init_og_after_min = 0.1
  exit_after_min = init_og_after_min + FLAGS.exit_after_min
  strategy_config = PnlPublisherConfig(load_clean_json(FLAGS.config_filename))

  strategy = LogPublishStrategy(strategy_config)
  return run_live(strategy=strategy,
                  og_config_name=FLAGS.og_config_name,
                  og_params=strategy_config.og_params,
                  init_order_gateway_after=datetime.timedelta(minutes=init_og_after_min),
                  exit_after=datetime.timedelta(minutes=exit_after_min))


if __name__ == '__main__':
  flags.DEFINE_string('og_config_name', None, 'og_config_name')

  flags.DEFINE_string('config_filename', '', '')

  flags.DEFINE_integer('exit_after_min', 30, '')

  logging.basicConfig(level='INFO', format='%(levelname)s %(asctime)s %(name)s] %(message)s')
  app.run(main)
